/**
 * alertify
 * An unobtrusive customizable JavaScript notification system
 *
 * @author Fabien Doiron <fabien.doiron@gmail.com>
 * @copyright Fabien Doiron 2013
 * @license MIT <http://opensource.org/licenses/mit-license.php>
 * @link http://fabien-d.github.com/alertify.js/
 * @module alertify
 * @version 0.3.11
 */
(function (global, undefined) {
    "use strict";


    var document = global.document,
        Alertify;

    Alertify = function () {

        var _alertify = {},
            dialogs   = {},
            isopen    = false,
            keys      = { ENTER: 13, ESC: 27, SPACE: 32 },
            queue     = [],
            $, btnCancel, btnOK, btnReset, btnResetBack, btnFocus, elCallee, elCover, elDialog, elLog, form, input, getTransitionEvent;

        /**
         * Markup pieces
         * @type {Object}
         */
        dialogs = {
            buttons : {
                holder : "<nav class=\"alertify-buttons\">{{buttons}}</nav>",
                submit : "<button type=\"submit\" class=\"alertify-button alertify-button-ok\" id=\"alertify-ok\">{{ok}}</button>",
                ok     : "<button class=\"alertify-button alertify-button-ok\" id=\"alertify-ok\">{{ok}}</button>",
                cancel : "<button class=\"alertify-button alertify-button-cancel\" id=\"alertify-cancel\">{{cancel}}</button>"
            },
            input   : "<div class=\"alertify-text-wrapper\"><input type=\"text\" class=\"alertify-text\" id=\"alertify-text\"></div>",
            message : "<p class=\"alertify-message\">{{message}}</p>",
            log     : "<article class=\"alertify-log{{class}}\">{{message}}</article>"
        };

        /**
         * Return the proper transitionend event
         * @return {String}    Transition type string
         */
        getTransitionEvent = function () {
            var t,
                type,
                supported   = false,
                el          = document.createElement("fakeelement"),
                transitions = {
                    "WebkitTransition" : "webkitTransitionEnd",
                    "MozTransition"    : "transitionend",
                    "OTransition"      : "otransitionend",
                    "transition"       : "transitionend"
                };

            for (t in transitions) {
                if (el.style[t] !== undefined) {
                    type      = transitions[t];
                    supported = true;
                    break;
                }
            }

            return {
                type      : type,
                supported : supported
            };
        };

        /**
         * Shorthand for document.getElementById()
         *
         * @param  {String} id    A specific element ID
         * @return {Object}       HTML element
         */
        $ = function (id) {
            return document.getElementById(id);
        };

        /**
         * Alertify private object
         * @type {Object}
         */
        _alertify = {

            /**
             * Labels object
             * @type {Object}
             */
            labels : {
                ok: "\u786e\u5b9a",
                cancel: "\u53d6\u6d88"
            },

            /**
             * Delay number
             * @type {Number}
             */
            delay : 5000,

            /**
             * Whether buttons are reversed (default is secondary/primary)
             * @type {Boolean}
             */
            buttonReverse : false,

            /**
             * Which button should be focused by default
             * @type {String}	"ok" (default), "cancel", or "none"
             */
            buttonFocus : "ok",

            /**
             * Set the transition event on load
             * @type {[type]}
             */
            transition : undefined,

            /**
             * Set the proper button click events
             *
             * @param {Function} fn    [Optional] Callback function
             *
             * @return {undefined}
             */
            addListeners : function (fn) {
                var hasOK     = (typeof btnOK !== "undefined"),
                    hasCancel = (typeof btnCancel !== "undefined"),
                    hasInput  = (typeof input !== "undefined"),
                    val       = "",
                    self      = this,
                    ok, cancel, common, key, reset;

                // ok event handler
                ok = function (event) {
                    if (typeof event.preventDefault !== "undefined") event.preventDefault();
                    common(event);
                    if (typeof input !== "undefined") val = input.value;
                    if (typeof fn === "function") {
                        if (typeof input !== "undefined") {
                            fn(true, val);
                        }
                        else fn(true);
                    }
                    return false;
                };

                // cancel event handler
                cancel = function (event) {
                    if (typeof event.preventDefault !== "undefined") event.preventDefault();
                    common(event);
                    if (typeof fn === "function") fn(false);
                    return false;
                };

                // common event handler (keyup, ok and cancel)
                common = function (event) {
                    self.hide();
                    self.unbind(document.body, "keyup", key);
                    self.unbind(btnReset, "focus", reset);
                    if (hasOK) self.unbind(btnOK, "click", ok);
                    if (hasCancel) self.unbind(btnCancel, "click", cancel);
                };

                // keyup handler
                key = function (event) {
                    var keyCode = event.keyCode;
                    if ((keyCode === keys.SPACE && !hasInput) || (hasInput && keyCode === keys.ENTER)) ok(event);
                    if (keyCode === keys.ESC && hasCancel) cancel(event);
                };

                // reset focus to first item in the dialog
                reset = function (event) {
                    if (hasInput) input.focus();
                    else if (!hasCancel || self.buttonReverse) btnOK.focus();
                    else btnCancel.focus();
                };

                // handle reset focus link
                // this ensures that the keyboard focus does not
                // ever leave the dialog box until an action has
                // been taken
                this.bind(btnReset, "focus", reset);
                this.bind(btnResetBack, "focus", reset);
                // handle OK click
                if (hasOK) this.bind(btnOK, "click", ok);
                // handle Cancel click
                if (hasCancel) this.bind(btnCancel, "click", cancel);
                // listen for keys, Cancel => ESC
                this.bind(document.body, "keyup", key);
                if (!this.transition.supported) {
                    this.setFocus();
                }
            },

            /**
             * Bind events to elements
             *
             * @param  {Object}   el       HTML Object
             * @param  {Event}    event    Event to attach to element
             * @param  {Function} fn       Callback function
             *
             * @return {undefined}
             */
            bind : function (el, event, fn) {
                if (typeof el.addEventListener === "function") {
                    el.addEventListener(event, fn, false);
                } else if (el.attachEvent) {
                    el.attachEvent("on" + event, fn);
                }
            },

            /**
             * Use alertify as the global error handler (using window.onerror)
             *
             * @return {boolean} success
             */
            handleErrors : function () {
                if (typeof global.onerror !== "undefined") {
                    var self = this;
                    global.onerror = function (msg, url, line) {
                        self.error("[" + msg + " on line " + line + " of " + url + "]", 0);
                    };
                    return true;
                } else {
                    return false;
                }
            },

            /**
             * Append button HTML strings
             *
             * @param {String} secondary    The secondary button HTML string
             * @param {String} primary      The primary button HTML string
             *
             * @return {String}             The appended button HTML strings
             */
            appendButtons : function (secondary, primary) {
                return this.buttonReverse ? primary + secondary : secondary + primary;
            },

            /**
             * Build the proper message box
             *
             * @param  {Object} item    Current object in the queue
             *
             * @return {String}         An HTML string of the message box
             */
            build : function (item) {
                var html    = "",
                    type    = item.type,
                    message = item.message,
                    css     = item.cssClass || "";

                html += "<div class=\"alertify-dialog\">";
                html += "<a id=\"alertify-resetFocusBack\" class=\"alertify-resetFocus\" href=\"#\">Reset Focus</a>";

                if (_alertify.buttonFocus === "none") html += "<a href=\"#\" id=\"alertify-noneFocus\" class=\"alertify-hidden\"></a>";

                // doens't require an actual form
                if (type === "prompt") html += "<div id=\"alertify-form\">";

                html += "<article class=\"alertify-inner\">";
                html += dialogs.message.replace("{{message}}", message);

                if (type === "prompt") html += dialogs.input;

                html += dialogs.buttons.holder;
                html += "</article>";

                if (type === "prompt") html += "</div>";

                html += "<a id=\"alertify-resetFocus\" class=\"alertify-resetFocus\" href=\"#\">Reset Focus</a>";
                html += "</div>";

                switch (type) {
                    case "confirm":
                        html = html.replace("{{buttons}}", this.appendButtons(dialogs.buttons.cancel, dialogs.buttons.ok));
                        html = html.replace("{{ok}}", this.labels.ok).replace("{{cancel}}", this.labels.cancel);
                        break;
                    case "prompt":
                        html = html.replace("{{buttons}}", this.appendButtons(dialogs.buttons.cancel, dialogs.buttons.submit));
                        html = html.replace("{{ok}}", this.labels.ok).replace("{{cancel}}", this.labels.cancel);
                        break;
                    case "alert":
                        html = html.replace("{{buttons}}", dialogs.buttons.ok);
                        html = html.replace("{{ok}}", this.labels.ok);
                        break;
                    default:
                        break;
                }

                elDialog.className = "alertify alertify-" + type + " " + css;
                elCover.className  = "alertify-cover";
                return html;
            },

            /**
             * Close the log messages
             *
             * @param  {Object} elem    HTML Element of log message to close
             * @param  {Number} wait    [optional] Time (in ms) to wait before automatically hiding the message, if 0 never hide
             *
             * @return {undefined}
             */
            close : function (elem, wait) {
                // Unary Plus: +"2" === 2
                var timer = (wait && !isNaN(wait)) ? +wait : this.delay,
                    self  = this,
                    hideElement, transitionDone;

                // set click event on log messages
                this.bind(elem, "click", function () {
                    hideElement(elem);
                });
                // Hide the dialog box after transition
                // This ensure it doens't block any element from being clicked
                transitionDone = function (event) {
                    event.stopPropagation();
                    // unbind event so function only gets called once
                    self.unbind(this, self.transition.type, transitionDone);
                    // remove log message
                    elLog.removeChild(this);
                    if (!elLog.hasChildNodes()) elLog.className += " alertify-logs-hidden";
                };
                // this sets the hide class to transition out
                // or removes the child if css transitions aren't supported
                hideElement = function (el) {
                    // ensure element exists
                    if (typeof el !== "undefined" && el.parentNode === elLog) {
                        // whether CSS transition exists
                        if (self.transition.supported) {
                            self.bind(el, self.transition.type, transitionDone);
                            el.className += " alertify-log-hide";
                        } else {
                            elLog.removeChild(el);
                            if (!elLog.hasChildNodes()) elLog.className += " alertify-logs-hidden";
                        }
                    }
                };
                // never close (until click) if wait is set to 0
                if (wait === 0) return;
                // set timeout to auto close the log message
                setTimeout(function () { hideElement(elem); }, timer);
            },

            /**
             * Create a dialog box
             *
             * @param  {String}   message        The message passed from the callee
             * @param  {String}   type           Type of dialog to create
             * @param  {Function} fn             [Optional] Callback function
             * @param  {String}   placeholder    [Optional] Default value for prompt input field
             * @param  {String}   cssClass       [Optional] Class(es) to append to dialog box
             *
             * @return {Object}
             */
            dialog : function (message, type, fn, placeholder, cssClass) {
                // set the current active element
                // this allows the keyboard focus to be resetted
                // after the dialog box is closed
                elCallee = document.activeElement;
                // check to ensure the alertify dialog element
                // has been successfully created
                var check = function () {
                    if ((elLog && elLog.scrollTop !== null) && (elCover && elCover.scrollTop !== null)) return;
                    else check();
                };
                // error catching
                if (typeof message !== "string") throw new Error("message must be a string");
                if (typeof type !== "string") throw new Error("type must be a string");
                if (typeof fn !== "undefined" && typeof fn !== "function") throw new Error("fn must be a function");
                // initialize alertify if it hasn't already been done
                this.init();
                check();

                queue.push({ type: type, message: message, callback: fn, placeholder: placeholder, cssClass: cssClass });
                if (!isopen) this.setup();

                return this;
            },

            /**
             * Extend the log method to create custom methods
             *
             * @param  {String} type    Custom method name
             *
             * @return {Function}
             */
            extend : function (type) {
                if (typeof type !== "string") throw new Error("extend method must have exactly one paramter");
                return function (message, wait) {
                    this.log(message, type, wait);
                    return this;
                };
            },

            /**
             * Hide the dialog and rest to defaults
             *
             * @return {undefined}
             */
            hide : function () {
                var transitionDone,
                    self = this;
                // remove reference from queue
                queue.splice(0,1);
                // if items remaining in the queue
                if (queue.length > 0) this.setup(true);
                else {
                    isopen = false;
                    // Hide the dialog box after transition
                    // This ensure it doens't block any element from being clicked
                    transitionDone = function (event) {
                        event.stopPropagation();
                        // unbind event so function only gets called once
                        self.unbind(elDialog, self.transition.type, transitionDone);
                    };
                    // whether CSS transition exists
                    if (this.transition.supported) {
                        this.bind(elDialog, this.transition.type, transitionDone);
                        elDialog.className = "alertify alertify-hide alertify-hidden";
                    } else {
                        elDialog.className = "alertify alertify-hide alertify-hidden alertify-isHidden";
                    }
                    elCover.className  = "alertify-cover alertify-cover-hidden";
                    // set focus to the last element or body
                    // after the dialog is closed
                    elCallee.focus();
                }
            },

            /**
             * Initialize Alertify
             * Create the 2 main elements
             *
             * @return {undefined}
             */
            init : function () {
                // ensure legacy browsers support html5 tags
                document.createElement("nav");
                document.createElement("article");
                document.createElement("section");
                // cover
                if ($("alertify-cover") == null) {
                    elCover = document.createElement("div");
                    elCover.setAttribute("id", "alertify-cover");
                    elCover.className = "alertify-cover alertify-cover-hidden";
                    document.body.appendChild(elCover);
                }
                // main element
                if ($("alertify") == null) {
                    isopen = false;
                    queue = [];
                    elDialog = document.createElement("section");
                    elDialog.setAttribute("id", "alertify");
                    elDialog.className = "alertify alertify-hidden";
                    document.body.appendChild(elDialog);
                }
                // log element
                if ($("alertify-logs") == null) {
                    elLog = document.createElement("section");
                    elLog.setAttribute("id", "alertify-logs");
                    elLog.className = "alertify-logs alertify-logs-hidden";
                    document.body.appendChild(elLog);
                }
                // set tabindex attribute on body element
                // this allows script to give it focus
                // after the dialog is closed
                document.body.setAttribute("tabindex", "0");
                // set transition type
                this.transition = getTransitionEvent();
            },

            /**
             * Show a new log message box
             *
             * @param  {String} message    The message passed from the callee
             * @param  {String} type       [Optional] Optional type of log message
             * @param  {Number} wait       [Optional] Time (in ms) to wait before auto-hiding the log
             *
             * @return {Object}
             */
            log : function (message, type, wait) {
                // check to ensure the alertify dialog element
                // has been successfully created
                var check = function () {
                    if (elLog && elLog.scrollTop !== null) return;
                    else check();
                };
                // initialize alertify if it hasn't already been done
                this.init();
                check();

                elLog.className = "alertify-logs";
                this.notify(message, type, wait);
                return this;
            },

            /**
             * Add new log message
             * If a type is passed, a class name "alertify-log-{type}" will get added.
             * This allows for custom look and feel for various types of notifications.
             *
             * @param  {String} message    The message passed from the callee
             * @param  {String} type       [Optional] Type of log message
             * @param  {Number} wait       [Optional] Time (in ms) to wait before auto-hiding
             *
             * @return {undefined}
             */
            notify : function (message, type, wait) {
                var log = document.createElement("article");
                log.className = "alertify-log" + ((typeof type === "string" && type !== "") ? " alertify-log-" + type : "");
                log.innerHTML = message;
                // append child
                elLog.appendChild(log);
                // triggers the CSS animation
                setTimeout(function() { log.className = log.className + " alertify-log-show"; }, 50);
                this.close(log, wait);
            },

            /**
             * Set properties
             *
             * @param {Object} args     Passing parameters
             *
             * @return {undefined}
             */
            set : function (args) {
                var k;
                // error catching
                if (typeof args !== "object" && args instanceof Array) throw new Error("args must be an object");
                // set parameters
                for (k in args) {
                    if (args.hasOwnProperty(k)) {
                        this[k] = args[k];
                    }
                }
            },

            /**
             * Common place to set focus to proper element
             *
             * @return {undefined}
             */
            setFocus : function () {
                if (input) {
                    input.focus();
                    input.select();
                }
                else btnFocus.focus();
            },

            /**
             * Initiate all the required pieces for the dialog box
             *
             * @return {undefined}
             */
            setup : function (fromQueue) {
                var item = queue[0],
                    self = this,
                    transitionDone;

                // dialog is open
                isopen = true;
                // Set button focus after transition
                transitionDone = function (event) {
                    event.stopPropagation();
                    self.setFocus();
                    // unbind event so function only gets called once
                    self.unbind(elDialog, self.transition.type, transitionDone);
                };
                // whether CSS transition exists
                if (this.transition.supported && !fromQueue) {
                    this.bind(elDialog, this.transition.type, transitionDone);
                }
                // build the proper dialog HTML
                elDialog.innerHTML = this.build(item);
                // assign all the common elements
                btnReset  = $("alertify-resetFocus");
                btnResetBack  = $("alertify-resetFocusBack");
                btnOK     = $("alertify-ok")     || undefined;
                btnCancel = $("alertify-cancel") || undefined;
                btnFocus  = (_alertify.buttonFocus === "cancel") ? btnCancel : ((_alertify.buttonFocus === "none") ? $("alertify-noneFocus") : btnOK),
                    input     = $("alertify-text")   || undefined;
                form      = $("alertify-form")   || undefined;
                // add placeholder value to the input field
                if (typeof item.placeholder === "string" && item.placeholder !== "") input.value = item.placeholder;
                if (fromQueue) this.setFocus();
                this.addListeners(item.callback);
            },

            /**
             * Unbind events to elements
             *
             * @param  {Object}   el       HTML Object
             * @param  {Event}    event    Event to detach to element
             * @param  {Function} fn       Callback function
             *
             * @return {undefined}
             */
            unbind : function (el, event, fn) {
                if (typeof el.removeEventListener === "function") {
                    el.removeEventListener(event, fn, false);
                } else if (el.detachEvent) {
                    el.detachEvent("on" + event, fn);
                }
            }
        };

        return {
            alert   : function (message, fn, cssClass) { _alertify.dialog(message, "alert", fn, "", cssClass); return this; },
            confirm : function (message, fn, cssClass) { _alertify.dialog(message, "confirm", fn, "", cssClass); return this; },
            extend  : _alertify.extend,
            init    : _alertify.init,
            log     : function (message, type, wait) { _alertify.log(message, type, wait); return this; },
            prompt  : function (message, fn, placeholder, cssClass) { _alertify.dialog(message, "prompt", fn, placeholder, cssClass); return this; },
            success : function (message, wait) { _alertify.log(message, "success", wait); return this; },
            error   : function (message, wait) { _alertify.log(message, "error", wait); return this; },
            set     : function (args) { _alertify.set(args); },
            labels  : _alertify.labels,
            debug: _alertify.handleErrors,
            close: function (elem) { _alertify.close(elem, 3000); }
        };
    };

    // AMD and window support
    if (typeof define === "function") {
        define([], function () { return new Alertify(); });
    } else if (typeof global.alertify === "undefined") {
        global.alertify = new Alertify();
    }

}(this));
